package com.kamistoat.meimeistore.commonsecurity.aspect;

import com.kamistoat.meimeistore.commoncore.constant.SecurityConstants;
import com.kamistoat.meimeistore.commoncore.exception.InnerAuthException;
import com.kamistoat.meimeistore.commoncore.utils.ServletUtils;
import com.kamistoat.meimeistore.commoncore.utils.StringUtils;
import com.kamistoat.meimeistore.commonsecurity.annotation.InnerAuth;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

/**
 * 对被 @InnerAuth注解的内部方法的调用进行验证，要求请求来源为内部来源。使用AOP切面技术
 * <p>
 * 使用 @Aspect注解，声明这个类将会别作为一个切面被容器织入到目标对象.
 * TODO AOP编程很重要，需要看相关知识学习。这里简单介绍 @Aspect 和 @Around的原理。
 * <p>
 * 切面--一种预埋的接入点，允许容器在运行时的任意时刻访问这个接入点。
 * 切面可以简单理解成拦截器，被切面包裹的任何内容(不管是类/方法/注解)相当于被一个拦截器所包裹，在方法/类/注解被执行的前/后阶段，容器都可以对其进行拦截，并执行切面定义的动作。
 * 但是@Aspect注解单独使用是没有任何效果的，因为它只是声明被注解的类会成为一个切面，换种理解就是声明被注解的类会成为一个拦截器。
 * 但是拦截器拦截哪些事件，拦截后执行哪些动作，这些需要在类内部使用 切点注解。
 * 三大切点注解: @Around(环绕) @Before(前置) @After(后置)
 */
@Aspect
@Component
public class InnerAuthAspect implements Ordered {
    /**
     * 环绕切点。@Around是配合@Aspect一起使用的。
     * 在 value中指定要拦截的条件。
     * 例如 "@annotation(com.kamistoat.meimeistore.commonsecurity.annotation.InnerAuth)" 意思是当要执行或访问的方法/类上带有 @InnerAuth注解时。
     * 例如 "execution(* com.kamistoat.controller.*.*(..))" 意思是执行com.kamistoat.controller包下的任意类(对应第一个*)下的任意方法(对应第二个*和无限制的入参)时
     * 方法必需的输入参数是 ProceedingJoinPoint，就是被拦截的接入点。调用 ProceedingJoinPoint.proceed() 执行被拦截的内容
     *
     * P.S. 如果@Around()为 "@annotation(ano)" 的形式，有两种情况:
     *   如果被 @Around()注解的方法的输入参数没有ano，则@Around中的ano必须写成绝对路径。
     *   如果被 @Around()注解的方法的输入参数有 Ano anoParam，则@Around中的ano必须写成和输入参数同名的 anoParam。
     *
     * P.S. @Around()中允许使用 && 和 || 链接拦截条件。见PreAuthorizeAspect类
     *
     * @param proceedingJoinPoint 拦截接入点
     * @param innerAuth           @InnerAuth注解
     */
    @Around(value = "@annotation(innerAuth)")
    public Object innerAround(ProceedingJoinPoint proceedingJoinPoint, InnerAuth innerAuth) throws Throwable {
        /**
         * 这里验证 @InnerAuth 的逻辑倒是很简单，这是我们自定的。
         * 就是保证在调用 被@InnerAuth 标注的方法前，手动向请求Header中添加一个 k-v对: from_source - inner 。
         * 然后切面检查 被@InnerAuth 标注的方法接收的请求Header中是否包含上述k-v对。
         * 就是一个自定义的二次握手的过程，不是啥自带的高级东西。
         */
        // 从当前请求头中提取请求来源
        String source = ServletUtils.getHeader(SecurityConstants.FROM_SOURCE);
        // @InnerAuth 注解的方法必须得是内部调用
        if (!StringUtils.equals(SecurityConstants.INNER, source)) {
            throw new InnerAuthException("失败! 该方法只允许内部调用，当前请求不具备内部权限!");
        }
        // 获取请求头中的user信息
        String userId = ServletUtils.getHeader(SecurityConstants.DETAILS_USER_ID);
        String userName = ServletUtils.getHeader(SecurityConstants.DETAILS_USERNAME);
        // 用户是否匿名校验
        if (innerAuth.isUser() && (StringUtils.isEmpty(userId) || StringUtils.isEmpty(userName))) {
            throw new InnerAuthException("失败! 该方法只允许内部非匿名用户调用，当前请求由内部匿名用户发起!");
        }
        // 全部通过则执行拦截内容
        /**
         * 是要是包含前置环节的切面，都可以使用 ProceedingJoinPoint.proceed() 继续原始内容的执行。
         * 如果切面拦截的是方法，在这里也可以修改方法的输入参数后，使用新的参数继续执行原始内容
         */
        Object proceedResult = proceedingJoinPoint.proceed();
        /**
         * @Around注解允许在执行完拦截内容后，继续某些处理，甚至可以修改上面的proceedResult的值后再返回
         */
        return proceedResult;
    }

    /**
     * 实现 Ordered接口的唯一方法。
     * 该切面拦截 需要在 PreAuthorizeAspect的 权限认证切面拦截前执行。
     * 因为在这儿通不过的请求，在权限认证肯定更通不过(外部用户无内部权限，匿名用户压根就没权限)。
     * 所以要在权限认证前，完成这里的拦截，提升系统性能。
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}




















